package protocol;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.PortUnreachableException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;

import protocol.data.DataPacket;
import protocol.data.Packet;
import protocol.data.ReplyPacket;
import virtualdatagramsocket.VirtualDatagramSocket;
import files.FileHandler;
import files.SentDataInfo;

public class Sender extends Thread {
	private VirtualDatagramSocket senderSocket;
	private FileHandler filehandler;
	private final int MAX_RETRANSMITS = 4;
	private final int MAX_SENT_PACKETS = 4;
	private int sequenceNumber = 1;
	private ArrayList<SentDataInfo> list ;

	public void setSequenceNumber(int sequenceNumber) {
		this.sequenceNumber = sequenceNumber;
	}

	public int getSequenceNumber() {
		return sequenceNumber;
	}

	
	public void setList(ArrayList<SentDataInfo> list) {
		this.list = list;
	}

	public ArrayList<SentDataInfo> getList() {
		return list;
	}

	public void setFilehandler(FileHandler filehandler) {
		this.filehandler = filehandler;
	}

	public FileHandler getFilehandler() {
		return filehandler;
	}

	public void setSenderSocket(VirtualDatagramSocket senderSocket) {
		this.senderSocket = senderSocket;
	}

	public VirtualDatagramSocket getSenderSocket() {
		return senderSocket;
	}
	
	public Sender(VirtualDatagramSocket senderSocket, String filename, InetAddress fellowAddress, int fellowPort) throws SocketException, FileNotFoundException {
		this.setSenderSocket(senderSocket);
		this.setFilehandler(new FileHandler(filename));
		this.getSenderSocket().connect(fellowAddress, fellowPort);
		this.getSenderSocket().setSoTimeout(5000);
		this.setList(new ArrayList<SentDataInfo>());
		
	}

	/**
	 * Reads the file to get the next chunk to send and wraps it inside
	 * of a DatagramPacket...
	 * @param sequenceNumber
	 * @return DatagramPacket or null if all fails
	 */
	public DatagramPacket nextDataPacket(int sequenceNumber) {
		try {
			DataPacket p = new DataPacket();
			byte[] data = this.getFilehandler().read(1437);
			
			if(data == null) return null;//data = new byte[1437];
			p.setData(data, true);
			p.setDataLength(data.length);
			p.setSequenceNumber(sequenceNumber);
			System.out.println("Sent packet : " + p.getSequenceNumber());
			
			Packet packet = new Packet();
			packet.setData(p);
			packet.setType((byte)1);
			
			return new DatagramPacket(packet.build(), packet.build().length);

		} catch (IOException e) {
			
		}
		return null;
	}
	
	public boolean retransmit(int sequenceNumber) {
			DataPacket p = new DataPacket();
			byte[] data = null;
			
			try {
				data = this.getFilehandler().readFrom(1437,(sequenceNumber-1)*1437);
			} catch (IOException e) {
				System.err.println("Retransmit, readFrom : " +e.getMessage());
				return false;
			}
			
			if(data == null) return false;
			p.setData(data, true);
			p.setDataLength(data.length);
			p.setSequenceNumber(sequenceNumber);
			
			Packet packet = new Packet();
			packet.setData(p);
			packet.setType((byte)1);
			if(this.send(new DatagramPacket(packet.build(),packet.build().length))) return true;
			
			return false;

		
	}
	
	public void packetAcked(ReplyPacket reply) {
		int i = reply.getSequenceNumber();
	    deleteInfo(i);
	}
	
	public SentDataInfo deleteInfo(int sequenceNumber) {
	    for(SentDataInfo sdi : list) {
		   if(sdi.getSequenceNumber() == sequenceNumber) {
			   list.remove(sdi);
			   return sdi;
		   }
	    }    
	    return null;
	}
	
	public void deleteAndAddCount(int sequenceNumber) {
		SentDataInfo s = deleteInfo(sequenceNumber);
		if( s != null ) {
			s.addRetransmitCount();
		    this.getList().add(s);
		}
	}
	
	private boolean send(DatagramPacket p) {
		try {
			this.getSenderSocket().send(p);
			return true;
		} catch (IOException e) {
			System.err.println("Send() : " + e.getMessage());
			return false;
		}
	}
	
	private boolean retransmitFirst() {
		if(list.size() != 0){
			SentDataInfo s = list.get(0);
			if (s != null){
				if (s.getRetransmitCount() == MAX_RETRANSMITS) return false;
				else {
					if(retransmit(s.getSequenceNumber())){
					   System.out.println("retransmitted " + s.getSequenceNumber());
					   deleteAndAddCount(s.getSequenceNumber());
					   return true;
					}
				}
			} 
		}
		return false;
	}
	
	/**
	 * Complete the whole receive process with acks.
	 */
	public void run() {
		boolean EOF = false;
		
		while(this.getSenderSocket().isConnected()){
			//We still have packets to send.
			if(list.size() < MAX_SENT_PACKETS && !EOF) {
				DatagramPacket p = nextDataPacket(sequenceNumber);
				if (p != null){
					if(this.send(p)) list.add(new SentDataInfo(sequenceNumber++));
				} else EOF = true;
			} else if(list.size() == 0 && EOF) {
				return;
			} else {
				DatagramPacket p = new DatagramPacket(new byte[1500], 1500);
				try {
					this.getSenderSocket().receive(p);
					Packet ack = new Packet();
					ack.parse(p.getData());
					
					switch(ack.getType()) {
					case 2:
						packetAcked((ReplyPacket)ack.getData());
						break;
					case 1:
						ReplyPacket reply = (ReplyPacket)ack.getData();
						if(reply == null) break;
						// When we need to send a packet again, let's not add the 
						// retransmission count.
						System.out.println("Crc failure in packet number " + reply.getSequenceNumber());
						if(retransmit(reply.getSequenceNumber())){
							SentDataInfo s = this.deleteInfo(reply.getSequenceNumber());
							if( s != null) this.getList().add(s);
						}
						break;
					default:
						break;
					}
				} catch(PortUnreachableException ex) {
					System.out.println("Fellow port unreachable, quit transfer.");
					System.err.println("Fellow port became unreachable, quit transfer : " + ex.getMessage());
					return;
				} catch(SocketTimeoutException e) {
					retransmitFirst();
				} catch (IOException e) {
					System.err.println("Run, receive : " + e.getMessage());
				}
			}
		}
	}
	
	public void finalize() throws Throwable {
		this.setFilehandler(null);
		if(this.getSenderSocket() != null && this.getSenderSocket().isConnected()) {
			this.getSenderSocket().disconnect();
		}
		this.setSenderSocket(null);
		if(this.getFilehandler() != null) {
			try{
			    this.getFilehandler().close();
			} catch(IOException ex) {
				ex.printStackTrace();
			}
		}
		this.setFilehandler(null);
		super.finalize();
	}

}

